const RFID = require("./RFID");
const {Log} = require("./utils")

class Carousel {
  constructor(config) {
    this.pointer = -1;
    this.laps = -1;
    this.totalplates = config.totalplates;
    this.threshold = config.threshold;
    this.MatchPoint = config.MatchPoint;
    this.MatchPointScreenStart = config.MatchPointScreenStart;
    this.MatchPointScreenOffsetPix = config.MatchPointScreenOffsetPix;
    this.screen = config.screen;
    this.matchedWindows = new Map(); // Store window - lpc mapping
    this.baggageInfo = new Map(); // Store baggageInfoObj - lpc mapping
    this.candidateWindows = new Map();
    this.plates = new Array(this.totalplates).fill(null).map((val, ind) => new Plate(ind));
    // Set plates' prev reference to form a circular linked list
    this.plates.forEach((plate, index) => {
      plate.prev = this.plates[(index - 1 + this.totalplates) % this.totalplates];
      plate.next = this.plates[(index + 1) % this.totalplates];
    });

    this.detectors = config.detector.map((val, index) => 
      new Detector(index, val.position, val.type, val.beforeRFID, val.offset,val.scale)
    );
    this.rfid = new RFID(config);
    this.log = new Log();
  }

  newPointer(plcdata){
    const log = console.log;
    const date = new Date();
    const hh = String(date.getHours());
    const mm = String(date.getMinutes());
    const ss = String(date.getSeconds());
    const ms = String(date.getMilliseconds()).substring(0,1);
    this.timestamp = `${hh}:${mm}:${ss}.${ms}`;
    const {data} = plcdata;
    this.pointer++;
    if (this.pointer >= this.totalplates) {
      this.pointer = 0;
      this.laps += 1;
    }
    if (this.pointer % 100 === 0) {
     console.log('pointer=',this.pointer,'time=',this.timestamp); 
    }
    this.log.new(this.timestamp);
    const logdata = {
      time: this.timestamp,
      laps: this.laps,
      data,
      pointer: this.pointer
    }
    console.log(JSON.stringify(logdata, null, 0));

    this.rfid.newpointer(this.pointer);
    this.dealDetectorData(data);
    this.dealPlateUnderRFID();
    // this.clearWindows(); // 0330：改为在鳞片删除窗口时进行清除
    //由于matchpoint可能在屏幕内，需要在匹配后及时发送消息，因此将screen_baggageinfo预先定义，作为参数传递给两个函数
    const screen_baggageInfo = {};
    this.dealMatchPoint(screen_baggageInfo);
    this.baggageEnterScreen(screen_baggageInfo);
    /**
     * 问题：对于MatchPoint和传感器、RFID在屏幕范围内的转盘，需要发送删除消息，以免残留
     */
    return {pointer: this.pointer,screen_baggageInfo};
  }
  dealDetectorData(data) {
    const log = console.log;
    this.detectors.forEach((detector, index) => {
      const curPlateNo = this.getScaleValue(detector.position);
      const curPlate   = this.plates[curPlateNo];
      const height = detector.getHeight(data[index]);
      let deltaHeight = height - curPlate.height;
      if (Math.abs(deltaHeight) < this.threshold.deltaHeight) {
        deltaHeight = 0;
      }

      curPlate.height = height;
      // If height is 0, clear plate window information directly
      if (height <= this.threshold.minWindowHeight) {
        if (curPlate.windows.size > 0) {
          console.log(`D${index}P${curPlateNo}:H=0->DeleteW:${Array.from(curPlate.windows.keys())}`);
          curPlate.windows.forEach((height, windowID) => {
            curPlate.delWindow(windowID,this.baggageInfo);
          });
          curPlate.windows = new Map();
        }
      }

      if (deltaHeight > 0) {
        // If no current window ID, create a new window
        if (detector.curWindow === null) {
            detector.curWindow = {
              id: `${index}-${this.timestamp}`,
              createdByMain: false,
              Lenth: 0,
              startPlate: curPlateNo,
              plates: new Set(),
              layer: -1,
            };
            // For tracking sensors, can generate ID or merge with previous plate's window, but don't put in matching list
            if (detector.type === 'tracker') {
              const prevPlate = curPlate.prev;
              // 如果高度比前一个鳞片高出的部分大于行李阈值，也就是deltaH比较大，认为是比前面的行李加了一层行李，不跟前面的合并
              // 暂时不考虑这个情况，直接尝试合并
              if (true || (height - prevPlate.height) < this.threshold.minWindowHeight) {
                const lastEntry = Array.from(prevPlate.windows.entries()).pop();
                if (lastEntry) {
                  if (this.baggageInfo.has(lastEntry[0])) {
                    detector.curWindow = this.baggageInfo.get(lastEntry[0]);
                  } else {
                    detector.curWindow.id = lastEntry[0]; // 获取键值对中的键（windowID）
                  }
                    console.log(`D${index}P${curPlateNo}:Merged to ${detector.curWindow.id}`);
                } else {
                  detector.curWindow.id += '-tracker';
                  console.log(`D${index}P${curPlateNo}:TrckerCreate ${detector.curWindow.id}`);
                }
              } 
            } else { // If it's a generating sensor, create window directly
              detector.curWindow.createdByMain = true;
              // this.candidateWindows.set(detector.curWindow.id, newWindow);
              console.log(`D${index}P${curPlateNo}:${detector.curWindow.id} is created`);
          }
        }
        let layer = 0;
        curPlate.windows.forEach((height, windowID) => {
          if (windowID.substr(-7) != 'tracker' && windowID != detector.curWindow.id) {
            layer += 1;
          }
        });
        console.log(`D${index}P${curPlateNo}:Layer=${layer},WindowList: ${Array.from(curPlate.windows.keys())}`);
        detector.curWindow.layer = Math.max(layer, detector.curWindow.layer);
        detector.curWindow.endPlate = curPlateNo;
        detector.curWindow.Lenth += 1;
        detector.curWindow.plates.add(curPlateNo);
        curPlate.windows.set(detector.curWindow.id, deltaHeight);
        console.log(`D${index}P${curPlateNo}:belong to ${detector.curWindow.id}`);
        if(detector.curWindow.createdByMain && detector.curWindow.plates.size >= this.threshold.minWindowLength) {
          this.candidateWindows.set(detector.curWindow.id, detector.curWindow);
          console.log(`D${index}P${curPlateNo}:Window ${detector.curWindow.id} is a candidate`);
        }
      } else {
        if (detector.curWindow) {
          if (detector.curWindow.createdByMain) {
            if(detector.curWindow.plates.size < this.threshold.minWindowLength) {
              // 如果窗口长度小于阈值：找到第一片鳞片，如果高度跟再前一片鳞片相差不大，合并；否则删除
              const platesArray = Array.from(detector.curWindow.plates);
              const firstPlate = this.plates[platesArray[0]];
              console.log(`D${index}P${curPlateNo}:Window${detector.curWindow.id}(${platesArray[0]}-${platesArray[platesArray.length-1]}) is too short`);
              let mergeWindowID = '';
              if ((firstPlate.height - firstPlate.prev.height) < this.threshold.minWindowHeight) {
                const lastEntry = Array.from(firstPlate.prev.windows.entries()).pop();
                if (lastEntry) {
                  // invalidWindowID = detector.curWindow.id;
                  mergeWindowID = lastEntry[0]; // 获取键值对中的键（windowID）
                  console.log(`D${index}P${curPlateNo}:Found ${mergeWindowID} to merge`);
                }
              }
              for (let i = 0; i < platesArray.length; i++) {
                const plate = this.plates[platesArray[i]];
                plate.delWindow(detector.curWindow.id,this.baggageInfo);
                if (mergeWindowID) {
                  plate.windows.set(mergeWindowID, deltaHeight);
                  console.log(`D${index}P${plate.index}:merged to window ${mergeWindowID}`);
                } else {
                  console.log(`D${index}P${plate.index}:Delete window ${detector.curWindow.id}, not merged`); 
                }
              }
            }
          }
          console.log(`D${index}P${curPlateNo}:Window${detector.curWindow.id} ends,dh=${deltaHeight}`);
          detector.curWindow = null;
        }
        if (deltaHeight < 0) {
        /*
        For negative height difference, there are two considerations:
        For single-layer luggage, it's already handled in the height=0 case above;
        For multi-layer luggage, if all removed it's handled in height=0 case; if not all removed, must use RFID to distinguish and mark.
        20250310：需要考虑RFID在检测器前的情况，根据label状态？
        */
          if (curPlate.windows.size > 0) {
           if (detector.beforeRFID) { // 如果传感器在RFID之前，并且在覆盖范围之外，需要等到达RFID下方之后再判断
            // 遍历当前plate的所有window，将所有window的checkRFIDtoDelete都设置为true，因为部分鳞片的dh可能不满足小于0的条件
            curPlate.windows.forEach((height, windowID) => {
              let plate = curPlate;
              while (plate.windows.has(windowID)) {
                plate.checkRFIDtoDelete = true;;
                plate = plate.prev; 
                console.log(`D${index}P${plate.index}:checkRFIDtoDel:deltaH=${deltaHeight}, windows:${Array.from(curPlate.windows.keys())},before RFID.`);
              }
            });
           } else {  // 其它情况下，传感器应该装在RFID范围内，可以直接通过能否读到RFID来判断
            // 遍历当前plate的所有window，删除所有未被读取的window
            curPlate.windows.forEach((height, windowID) => {
              const baggageInfoObj = this.baggageInfo.get(windowID);
              const label = baggageInfoObj? baggageInfoObj.label: windowID;
              if (!this.rfid.scanning(label)) {
                curPlate.delWindow(windowID,this.baggageInfo);
                console.log(`D${index}P${curPlateNo}:delete window(${windowID},${label}): not scanning`);
              } else {
                console.log(`D${index}P${curPlateNo}:keep window(${windowID},${label}): scanning`);
              }
            });
           }
          }
        }
      }
    }); 
  }
  dealPlateUnderRFID() {
    const log = console.log;
    // 处理RFID下方的鳞片，也即传感器在RFID覆盖范围前方的情况
    const plateNoUnderRFID = this.getScaleValue(this.rfid.position)
    const plateUnderRFID = this.plates[plateNoUnderRFID];
    if (plateUnderRFID.checkRFIDtoDelete && plateUnderRFID.windows.size > 0) {
      plateUnderRFID.checkRFIDtoDelete = false;
      plateUnderRFID.windows.forEach((height,windowID) => {
        if (this.matchedWindows.has(windowID)) {
          const baggageInfoObj = this.baggageInfo.get(windowID);
          const label = baggageInfoObj? baggageInfoObj.label: windowID;
          if (!this.rfid.scanning(label)) {
            plateUnderRFID.delWindow(windowID,this.baggageInfo);
            console.log(`RFID cannot scan window${windowID} on plate${plateNoUnderRFID}, delete`);
          } else {
            console.log(`RFID can scan window${windowID} on plate${plateNoUnderRFID}, keep`);
          }
        }
      });
    }
  }
  dealMatchPoint(screen_baggageInfo) {
    /* Consider virtual concept MatchPoint:
    Matching here can handle inconsistencies between RFID and detector positions
    Multiple entry points may have multiple windows to multiple RFIDs: e.g., luggage entering from front entry,
    followed by luggage from rear entry that's touching or stacked on top of the first one
    This situation cannot be distinguished without bottom scanning
    */
  const log = console.log;
  const MatchPointPlateNo = this.getScaleValue(this.MatchPoint);
  const candidateLabels = new Map();
  // Iterate through RFID to update status
  this.rfid.labels.forEach((label, key) => {
    if (label.startPlate === -1 ) {
      label.startPlate = this.getScaleValue(this.rfid.startPlate);
      label.endPlate = this.getScaleValue(this.rfid.endPlate);
      console.log(`Label ${key} is new, startPlate: ${label.startPlate}, endPlate: ${label.endPlate}`);
    }
    /* If label status is NEW and MatchPoint is within range, add to candidateLabels
      Attention: label of NEW may not be the same as startplate === -1
    */
    if (label.status === 'NEW' &&
        label.lpc &&
        this.contains(label.startPlate,label.endPlate,MatchPointPlateNo)) {
          if (!candidateLabels.has(key)) {
            candidateLabels.set(key, label);
            console.log(JSON.stringify({Labels: Object.fromEntries(this.rfid.labels)}, null, 0));
            // console.log(JSON.stringify(Object.fromEntries(candidateLabels), null, 0));
      }
    }
  });
  if (candidateLabels.size > 0) {
    console.log(`L${this.laps}Pointer${this.pointer}:CandidateLabels-${Array.from(candidateLabels.keys())}`);
  }
  // const newWindows = new Map();
  const plateMatchPoint = this.plates[MatchPointPlateNo];
  plateMatchPoint.windows.forEach((height, windowID) => {
    if (this.candidateWindows.has(windowID)) {
      const window2match = this.candidateWindows.get(windowID);
      if (candidateLabels.size > 0) {
        const [firstLabel, firstValue] = candidateLabels.entries().next().value;
        window2match.label = firstLabel;
        window2match.flight = firstValue;
        // newWindows.set(windowID, window2match);
        candidateLabels.delete(firstLabel);
        this.rfid.labels.get(firstLabel).status = 'MATCHED';
        this.matchedWindows.set(windowID, firstLabel);
        const baggageInfoObj = this.rfid.labels.get(firstLabel).baggageInfoObj;
        if (baggageInfoObj) {
          baggageInfoObj.label = firstLabel;
          baggageInfoObj.windowID = windowID;
          baggageInfoObj.id = windowID;
          baggageInfoObj.plates = window2match.plates;
          baggageInfoObj.layer = window2match.layer;
          this.baggageInfo.set(windowID, baggageInfoObj);
          if (this.MatchPointScreenStart) {
            baggageInfoObj.screenOffset = this.MatchPointScreenOffsetPix;
            screen_baggageInfo[this.MatchPointScreenStart] = [baggageInfoObj]; //数组，与baggageEnterScreen对应
          }
        }
        this.candidateWindows.delete(windowID);
        console.log(`Matched ${windowID} to ${firstValue.lpc} on plate${MatchPointPlateNo}`);
      } else {
        // 如果未匹配窗口的最后一片鳞片到了MatchPoint都没有匹配上，后续不再匹配窗口
        if (window2match.endPlate === MatchPointPlateNo) {
          this.candidateWindows.delete(windowID);
          console.log(`MatchFailed until endplate,${windowID} removed from candidate`);
        }
      }
    }
  });
  // 对于经过了MatchPoint没有匹配上的label，
  if (candidateLabels.size > 0) {
    candidateLabels.forEach((labelInfo, label) => {
      if (labelInfo.endPlate === MatchPointPlateNo) {
        this.rfid.ignLablesTime.set(label, labelInfo.lastSeen);
        this.rfid.labels.delete(label);
        console.log(`IgnLabel ${label} endplate${MatchPointPlateNo} as not matched`);
      }
    });
  }
 
  }
  baggageEnterScreen(screen_baggageInfo){
    const plateArrayofBaggageMap = new Map();
    this.screen.forEach((screen) => {
      const plateNoScrStart = this.getScaleValue(screen.start);
      const plateScrStart = this.plates[plateNoScrStart];
      const winlist = plateScrStart.windowsStartHere();
      let baggage2send = [];
      if ((screen_baggageInfo.hasOwnProperty(screen.start))) {
        baggage2send = screen_baggageInfo[screen.start]; 
      } 
      this.baggageInfo.forEach((baggageInfoObj, winID) => {
        if (!plateArrayofBaggageMap.has(winID)) {
          plateArrayofBaggageMap.set(winID, Array.from(baggageInfoObj.plates));
        }
        const plateNo = plateArrayofBaggageMap.get(winID)[0];
        if (this.plates[plateNo].windows.has(winID)) {
          if (plateNo === plateNoScrStart) {
            if (baggageInfoObj.plates.size > this.threshold.minWindowLength) {
              //recalculate layer
              let maxLayer = 0;
              baggageInfoObj.plates.forEach((plateNo) => {
                const winIdArray = Array.from(this.plates[plateNo].windows.keys());
                const index = winIdArray.indexOf(winID);
                maxLayer = Math.max(maxLayer, index);
              })
              if (baggageInfoObj.layer !== maxLayer) {
                console.log(`Baggage${baggageInfoObj.lpc}(${winID}) Layer:${baggageInfoObj.layer}->${maxLayer}`);
                baggageInfoObj.layer = maxLayer;
              }
              baggage2send.push(baggageInfoObj);
              console.log(`Baggage${baggageInfoObj.lpc}(${winID}) enterScreen:${screen.start}`);
            } else {
              console.log(`WARN:Baggage${baggageInfoObj.lpc}(${winID}) too short to enterScreen:${screen.start}`);
            }
          }
        } else {
          baggageInfoObj.plates.delete(plateNo);
          console.log(`WARN:Baggage${baggageInfoObj.lpc}(${winID}) ISNOT on Plate${plateNo}`);
          if (baggageInfoObj.plates.size === 0) {
            this.baggageInfo.delete(winID);
            console.log(`WARN:Baggage${baggageInfoObj.lpc}(${winID}) is removed in baggageEnterScreen`); 
          }
        }
        });
        if (baggage2send.length > 0) {
          screen_baggageInfo[screen.start] = baggage2send;
        }
    });
    return screen_baggageInfo;
  }
  /**
   * Calculate value based on carousel scale or plate number
   * @param {number} plateno - Plate number
   * @param {number} refPointer - Reference pointer position
   * @returns {number} Calculation result
   */
  getScaleValue(plateno, refPointer = null) {
    let p = refPointer != null ? refPointer - plateno : this.pointer - plateno;
    if (p < 0) {
      p += this.totalplates;
    }
    return p;
  }
  /**
 * Check if a point is within specified range
 * @param {number} p - Point
 * @param {number} start - Start position
 * @param {number} end - End position
 * @returns {boolean}
 */
contains(start, end, p) {
  if (start < end) {
    return p >= start && p <= end;
  } else {
    return !(p < start && p > end);
  }
}

}
class Plate {
  constructor(index) {
    this.index = index;
    this.height = 0;
    this.heights = [];
    this.windows = new Map();
    this.prev = null; // 指向前一个plate的引用
    this.next = null; // 指向后一个plate的引用
    this.checkRFIDtoDelete = false;
  }
  /**
   * 本鳞片是否是某（几）个窗口的起始鳞片
   */
  windowsStartHere() {
    const result = [];
    this.windows.forEach((height,windowID) => {
      if (!this.prev.windows.has(windowID) && windowID.substr(-6)!='tracker') {
        result.push(windowID);
      }
    });
    return result;
  }
  windowsEndHere(){
    const result = [];
    this.windows.forEach((height,windowID) => {
      if (!this.next.windows.has(windowID)) {
        result.push(windowID);
      }
    });
    return result;
  }
  delWindow(windowID,baggageInfo) {
    this.windows.delete(windowID);
    if (baggageInfo.has(windowID)) {
      const baggageInfoObj = baggageInfo.get(windowID);
      baggageInfoObj.plates.delete(this.index);
      if (baggageInfoObj.plates.size === 0) {
        baggageInfo.delete(windowID);
        console.log(`P${this.index}: BaggageInfo${baggageInfoObj.lpc}(${windowID}) deleted`);
      }
    }
    // console.log(`P${this.index}: Window ${windowID} deleted`);
  }
}
class Detector {
  /**
   * 创建一个新的检测器实例
   * @param {number} id - 检测器ID
   * @param {number} position - 检测器位置
   * @param {boolean} type - 检测器类型（生成/追踪）
   * @param {number} offset - 偏移量
   * @param {number} scale - 比例因子
   */
  constructor(id, position, type, beforeRFID, offset, scale) {
    this.id = id;
    this.position = position;
    this.type = type;
    this.beforeRFID = beforeRFID;
    this.offset = offset;
    this.scale = scale;
    this.curWindow = null;
  }
  /**
   * 将PLC返回值换算成实际高度
   * @param {number} plcValue - PLC返回值
   * @returns {number} 实际高度
   */
  getHeight(plcValue) {
    return Math.floor((plcValue - this.offset)/this.scale) ;
  }

}
module.exports = Carousel;
